package main.kotlin.singlePage

/**
 * @since 2018.3.30
 * @version
 * 单页内容解析器。包括下载、输入到MarkDown和HTML等内容
 */
import javafx.scene.control.TextArea
import main.kotlin.markdown.MdToHTML
import main.kotlin.markdown.PageContent
import main.kotlin.markdown.PageText
import java.io.File
import kotlin.text.Regex
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
import org.jsoup.nodes.Node
import org.apache.commons.io.FilenameUtils
import net.dongliu.requests.*
import net.dongliu.requests.exception.*
import mu.KotlinLogging
import org.jsoup.nodes.Document

import java.util.Scanner


private val logger = KotlinLogging.logger {}

class SinglePage constructor(private val url: String) {

    private var textArea:TextArea? = null
    /*
    次要构造函数，后边的this(url)是调用了其他构造函数（此处为主构造函数）
    然后次要构造函数，执行自己的功能
    */
    constructor(url: String, encoding: String): this(url) {
        this.encoding = encoding
    }

    constructor(url: String, encoding: String, textArea: TextArea? = null): this(url, encoding) {
        this.textArea = textArea
    }

    private val downloader = ImageDownloader()

    private var encoding: String = "utf-8"
            set(value) {
                field = value.toLowerCase()
            }

    var proxy: List<String> = listOf()
            set(value) {
                if (value.size == 2) {
                    field = value
                }
            }

    /*
    init内是初始化，在类最开始构造之前做的一些操作
    */
    init {
        println("Start to get page: $url")
    }

    private fun getContent(): String {
        var content = ""

        try{
            if (this.proxy.size == 2) {
                println("Using proxy")
                content = Requests.get(this.url)
                        .socksTimeout(30_000)
                        .connectTimeout(30_000)
                        .proxy(Proxies.socksProxy(this.proxy.first(), this.proxy.last().toInt()))
                        .send()
                        .charset(this.encoding)
                        .readToText()

                this.downloader.proxy = this.proxy
            } else {
                content = Requests.get(this.url)
                        .socksTimeout(30_000)
                        .connectTimeout(30_000)
                        .send()
                        .charset(this.encoding)
                        .readToText()
            }
        } catch (e: RequestsException) {
            when (textArea) {
                null -> logger.error { "Request $url failed\nerror message: ${e.message}"  }
                else -> {
                    this.textArea?.appendText("Request $url failed\nerror message: ${e.message}")
                }
            }
        }
        return content
    }

    /**
     * 提取每个帖子的发帖时间
     * @param soup: Jsoup，帖子整个页面的
     * @return 2018-4-19 这样的时间受
     */
    private fun getPostDate(soup: Document): String {

        val tr = soup.select("tr[class=tr1] > th > div[class=tipad]").first()
        var date = tr.text().split("Posted:")[1]
        date = date.split("[\\s|@]".toRegex())[0]

        var year = ""
        var month = ""
        var day = ""
        for (i in date.split("-+".toRegex())) {
            if (i.length == 4) year = i
            else if (month == "") month = i
            else day = i
        }

        return "$year-$month-$day"
    }

    /**
     * 通过正则提取heading标签的等级
     * @param html 字符串
     */
    private fun getHeadLevel(html: String): Int {
        var level = 0
        val pattern = Regex("<h\\d[^>]*>")

        val value: String? = pattern.find(html)?.groups?.get(0)?.value

        if (value != null) {
            level = value.replace(">", "").replace("<h", "").toInt()
        }

        return level
    }

    /**
     * @param tags Element，某个标签
     * @return Map<String, Int>,标题文本和等级构成的Map
     */
    private fun getHead(tags: Element): Map<String, Int> {
        val heads = tags.select("h1, h2, h3, h4, h5, h6")
        val headers = HashMap<String, Int>()
        // 注意用outerHTML
        for (h in heads) {
            headers.put(h.ownText(), this.getHeadLevel(h.outerHtml()))
        }
        return headers
    }

    /**
     * @param tags Element,某个父标签
     * @return 文本构成的list
     */
    private fun getTexts(tags: Element): ArrayList<String> {
        val text = ArrayList<String>()

        for (t in tags.childNodes()) {
            if (t is TextNode) {
                val ownText: String = t.text().trim()

                if (ownText != "") {
                    text.add(ownText)
                }
            }
        }
        return text
    }

    /**
     * @param tags Element,某个父标签
     * @return 链接地址和链接文本，构成的字典
     */
    private fun getLinks(tags: Element): Map<String, String> {
        val links = HashMap<String, String>()

        for ( link in tags.select("a[href]")) {
            // 尝试获取绝对路径
            val href: String = if (link.attr("abs:href") == "") {
                link.attr("href")
            } else {
                link.attr("abs:href")
            }

            links.put(href, link.ownText().trim())
        }
        return links
    }

    /**
     * @param tags Element,某个父标签
     * @return 图片文本和图片地址构成的标签，默认没有文本的情况下就使用图片地址的文件名
     */
    private fun getImgs(tags: Element): Map<String, String> {
        val links = HashMap<String, String>()
        val imgs = tags.select("img[src],input[src],img[data-src],img[data-link]")
        for ( link in imgs ) {
            // 尝试获取绝对路径

            val src: String = when {
                link.attr("abs:src") != "" -> link.attr("abs:src")
                link.attr("src") != "" -> link.attr("src")
                link.attr("data-src") != "" -> link.attr("data-src")
                link.attr("data-link") != "" -> link.attr("data-link")
                else -> ""
            }


            val text: String = if (link.ownText() != "") {
                link.ownText().trim()
            } else {
                FilenameUtils.getName(src)
            }

            links.put(text, src)
        }
        return links
    }


    /**
     * 分门别类的获取各种数据，整合到一个PageCoutent中
     */
    fun getTags(pageContent: String? = null): PageContent? {

        val content = when(pageContent) {
            null -> this.getContent()
            else -> pageContent
        }

        if (content == "") {
            return null
        }

        val soup = Jsoup.parse(content)

        // jsoup选取所需html代码块
        val tr = soup.select("tr.tr1.do_not_catch")

        val td = tr.select("th[height=100%] > table[width=100%] > tbody > tr > td[valign=top]").first()

        val mainContent = td.select("div.tpc_content.do_not_catch").first()

        // 统计各种元素
        val tittle: String = soup.select("title").first().ownText()

        val heading = getHead(td)

        val link = getLinks(mainContent)

        val text = getTexts(mainContent)

        val img = getImgs(mainContent)

        return PageContent(
                tittle, heading, link, img, text
        )
    }


    /**
     * @param tag 输入的标签，获取绝对路径的
     * @param attr 属性的名称，通常不是href就是src
     */
    private fun getAbsolutePath(tag: Node, attr: String): String {
        val value: String = tag.absUrl(attr).replace("\\", "/")

        when (value != "") {
            true -> return value
            false -> return tag.attr(attr)
        }
    }

    /**
     * 分为两类TextNode，和DataNode，分别处理， TextNode直接获取text，DataNode则尝试获取alt属性
     * @param tag: Node
     */
    private fun getTextofLink(tag: Node): String {
        when (tag.nodeName() == "a") {
            true -> {
                var text = (tag as TextNode).text()

                if ( text == "" ) {
                    text = FilenameUtils.getName(tag.absUrl("href"))
                }
                return text
            }
            false -> {
                return when (tag.hasAttr("alt")) {
                    true -> tag.attr("alt")
                    false -> FilenameUtils.getName(tag.absUrl("src"))
                }
            }
        }
    }


    /**
     * 通过递归，收集某个标签之内的所有文本、链接和图片
     * @param tags: 一列表的Node
     */
    private fun getAllElements(tags: List<Node>, outdir: String, title: String = "1024"): PageText {
        val wholePage = PageText(title)


        for (t in tags) {
            if (t.childNodeSize() > 0) { // 递归遍历子节点
                wholePage.add(this.getAllElements(t.childNodes(), outdir))
            } else if (t.nodeName() == "a") {
                // 获取链接
                wholePage.addLink(
                        this.getTextofLink(t),
                        this.getAbsolutePath(t, "href")
                )
            } else if (t.nodeName() in listOf("img", "input")) {
                // 获取图片
                if (
                        t.attr("src") != "" ||
                        t.attr("data-src") != "" ||
                        t.attr("data-link") != ""
                ) {
                    val imgLink = when {
                        t.attr("src") != "" -> this.getAbsolutePath(t, "src")
                        t.attr("data-src") != "" -> this.getAbsolutePath(t, "data-src")
                        t.attr("data-link") != "" -> this.getAbsolutePath(t, "data-link")
                        else -> ""
                    }

                    if (imgLink == "") continue

                    val imgFile = File(File(outdir, "img"), FilenameUtils.getName(imgLink)).toString()

                    wholePage.addImg(
                            this.getTextofLink(t), imgFile.replace(outdir, ".")

                    )

                    // 下载图片
                    try{
                        this.downloader.download(imgLink, imgFile)
                    } catch (e: Exception) {
                       val error =  "Error: ${e.message}\n"
                        when (textArea) {
                            null -> logger.error { error }
                            else -> {
                                val text = this.textArea
                                if (text != null) text.appendText(error)
                            }
                        }
                    }

                }
            } else if (t is TextNode){
                // 获取标题
                if (t.nodeName() in listOf("h1", "h2", "h3", "h4", "h5", "h6")) {
                    wholePage.addHeading(
                            t.text().trim(), getHeadLevel(t.outerHtml())
                    )
                } else {  // 获取其他普通文本
                    val text = t.text().trim()

                    if (text != "") {
                        wholePage.addText(text)
                    }
                }


            }
        }
        return wholePage
    }

    /**
     * 将所有文件存到文本中
     * @param directory: 下载输出目录。默认运行目录
     */
    fun saveTo(directory: String = System.getProperty("user.dir"), prefix:String = "same", style:String="self"): Boolean {
        // prepare Jsoup
        val content = getContent()

        if (content == "") {
            return true
        }
        val soup = Jsoup.parse(content)
        val date = getPostDate(soup)

        val pattern = Regex(" - .* \\| .* - \\w+\\.\\w+\$")
        var title: String = pattern.replace(soup.select("title").first().ownText(), "")

        // init results class
        val wholePage = PageText(title, this.url)

        val tr = soup.select("tr.tr1.do_not_catch")

        val td = tr.select(
                "th[height=100%] > table[width=100%] > tbody > tr > td[valign=top] > div.tpc_content.do_not_catch"
        )

        title = Regex("[/:*?\"<>|]").replace(title, "")

        val tmpDir = File(date, title)
        val outDir = File(directory, tmpDir.toString())
        val imgDir = File(outDir, "img")

        // check output directory
        if ( !imgDir.exists() ) { imgDir.mkdirs() }

        // 构建输出文件名
        var outPrefix: String = prefix
        if (prefix == "same") {
            outPrefix = title
        }

        wholePage.add(this.getAllElements(td.first().childNodes(), outDir.toString()))
        // save md
        wholePage.writeToMd(File(outDir.toString(), "$outPrefix.md").toString())

        // save html
        MdToHTML(wholePage, style).writeTo(File(outDir.toString(), "$outPrefix.html").toString())

        return true
    }

}

